module Pjs2 {
    export class Parser {
        keywords: Array<string> = ["function", "if"];
        operators = [[","], ["*", "/"], ['+', '-'], ["|"], ["="]];
        unaryOperators: Array<string> = ["++", "--", "!"];
        get incompleteScriptError(): string { return "incomplete_script_error"; }
        get invalidFormat(): string { return "invalid_format"; }

        public ReadScript(script: string, pos = new Pos(script.length - 1)): Script {
            var result = new Script();
            var start = pos.pos;
            while (pos.pos < pos.endpos) {
                trim(script, pos);
                if (script.charAt(pos.pos) == "}") break;
                if (script.charAt(pos.pos) == ")") throw "invalid_format: ')' occured in the script and no one knows how to parse it";
                if (script.charAt(pos.pos) == "\n") {
                    pos.pos++; continue;
                }
                result.commands.push(this.ReadCommand(script, pos));
            }
            result.source = script.substring(start, pos.pos);
            return result;
        }

        public ReadCommand(script: string, pos = new Pos(script.length - 1)): ICommand {
            var last = null;
            do {
                trim(script, pos);
                var expr: boolean = true;
                //jestli zaciname pismenkem, tak to bude asi prikaz
                if (charIsLetter(script.charAt(pos.pos)) || charIsNumber(script.charAt(pos.pos))) {
                    expr = false;
                    var ns = pos.pos;
                    while (!charIs(script.charAt(pos.pos), " (){}|\n;") && pos.pos <= pos.endpos) pos.pos++;
                    var str = script.substring(ns, pos.pos);
                    //kdyz nazev neni validni cislo, tak precteme prikaz
                    if (isNaN(parseFloat(str))) {
                        if (this.keywords.some(s => s == str)) {
                            last = this.ReadKeywordBlock(str, script, pos);
                        }
                        else {
                            var params = this.ReadParameters(script, pos);
                            last = new Call(str, params, last);
                        }
                    }
                    //jinak vratime pozici a vyraz precteme nekde jinde
                    else {
                        expr = true;
                        pos.pos = ns;
                    }
                }
                //jestli ne zkusime vyraz
                if (expr) last = this.ReadExpression(script, pos);

                //zopakujeme si to jestli je tu pipeline
            } while (script.charAt(pos.pos++) == "|")
            pos.pos--;

            return last;
        }

        public ReadExpression(script: string, pos = new Pos(script.length - 1), readTo: string = ""): ICommand {
            var ops = new Array<{ op: string; val: ICommand }>();
            while (true) {
                trim(script, pos);
                var actual: ICommand;
                var cast = new Array<string>();
                var operator: string = "";
                //precist operator
                if (script.charAt(pos.pos) == "-") {
                    //napr -eq -lt -gt ...
                    pos.pos++;
                    var ns = pos.pos;
                    while (charIsLetter(script.charAt(pos.pos)) && !charIs(script.charAt(pos.pos), readTo)) { pos.pos++; }
                    operator = "-" + script.substring(ns, pos.pos);
                }
                else {
                    //napr. + - * / , | && ||
                    var ns = pos.pos;
                    while (!charIs(script.charAt(pos.pos), " \n\0({$@#\"'[" + readTo) && !charIsLetter(script.charAt(pos.pos)) && !charIsNumber(script.charAt(pos.pos))) { pos.pos++; }
                    operator = script.substring(ns, pos.pos);
                }

                trim(script, pos);
                //recist statickou tridu nebo pretypovani
                while (script.charAt(pos.pos) == "[") {
                    pos.pos++;
                    var path = new Array<string>();
                    while (script.charAt(pos.pos) != "]") {
                        path.push(this.ReadString(script, pos, false, ".]").value);
                        if (script.charAt(pos.pos) == ".") pos.pos++;
                        else if (script.charAt(pos.pos) != "]") throw "']' or '.' expected at " + pos.pos;
                    }
                    pos.pos++;
                    trim(script, pos);
                    actual = new StaticClass(path);
                    if (script.charAt(pos.pos) == ":" && script.charAt(pos.pos + 1) == ":") {
                        pos.pos += 2;
                        actual = this.ReadDotNotation(script, actual, pos);
                    }
                    else if(charIsLetterNumberOr(script.charAt(pos.pos), "[({$")) {
                        var c = "";
                        path.forEach((p, i) => {
                            if(i != 0) c += ".";
                            c += p;
                        });
                        cast.push(c);
                    }
                }
                //přečíst vnořený příkaz
                if (script.charAt(pos.pos) == "(") {
                    pos.pos++;
                    actual = this.ReadCommand(script, pos);
                    if (script.charAt(pos.pos) != ")") throw "invalid brackets";
                    pos.pos++;
                }
                //přečíst ScriptBlock
                else if (script.charAt(pos.pos) == "{") {
                    pos.pos++;
                    var r: Script = this.ReadScript(script, pos);
                    if (script.charAt(pos.pos) != "}") throw "invalid curly brackets";
                    pos.pos++;
                    actual = new ScriptBlockCreate(r);
                }
                //precist string
                else if (script.charAt(pos.pos) == "\"" || script.charAt(pos.pos) == "'") {
                    actual = this.ReadString(script, pos);
                }
                //precist cislo
                else if (charIsNumber(script.charAt(pos.pos))) {
                    var ns = pos.pos;
                    while (charIsNumber(script.charAt(pos.pos)) || script.charAt(pos.pos) == ".") { pos.pos++ }
                    if (charIsLetter(script.charAt(pos.pos))) throw "invalid_number";
                    var num = parseFloat(script.substring(ns, pos.pos));
                    actual = new NumberCreate(num);
                }
                //precist promennou
                else if (script.charAt(pos.pos) == "$") {
                    pos.pos++;
                    var ns = pos.pos;
                    while (charIsLetterNumberOr(script.charAt(pos.pos), "_$^")) { pos.pos++; }
                    var name = script.substring(ns, pos.pos);
                    actual = new VariableCall(name);
                }
                else if (script.charAt(pos.pos) == "@") {
                    pos.pos++;
                    if (script.charAt(pos.pos) == "{") {
                        pos.pos++;
                        actual = this.ReadHashtable(script, pos);
                    }
                    else throw "not_supported_or_invalid_command";
                }

                if (actual == null) throw "not_supported_or_invalid_command";

                trim(script, pos);
                //precist teckovou notaci
                while (script.charAt(pos.pos) == ".") {
                    pos.pos++;
                    actual = this.ReadDotNotation(script, actual, pos);
                }
                
                if(script.charAt(pos.pos) == "=") {
                    pos.pos++;
                    trim(script, pos);
                    var next = this.ReadExpression(script, pos);
                    if(actual instanceof VariableCall) {
                        return new VariableSet(
                            (<VariableCall>actual).name,
                            next,
                            cast[0]
                        );
                    }
                    else if (actual instanceof Property) {
                        return new PropertySet(
                            <Property>actual,
                            next
                        );
                    }
                }
                
                cast.forEach(c => {
                    actual = new Cast(actual, c);
                });
                ops.push({ op: operator, val: actual });
                trim(script, pos);
                if (charIs(script.charAt(pos.pos), ")}|\n;\0" + readTo) || pos.pos >= pos.endpos)
                    break;
            }

            return this.OperatorsToCommand(ops);
        }

        public ReadScriptBlock(script: string, pos: Pos) {
            trim(script, pos);
            if (script.charAt(pos.pos) == "{") {
                pos.pos++;
                var r: Script = this.ReadScript(script, pos);
                if (script.charAt(pos.pos) != "}") throw "invalid curly brackets";
                pos.pos++;
                return new ScriptBlockCreate(r);
            }
            throw "'{' expected at position " + pos.pos;
        }

        public ReadString(script: string, pos: Pos = new Pos(script.length - 1), quoted: boolean = true, disabledChars: string = ""): StringCreate {
            var ech = script.charAt(pos.pos);
            if (!charIs(ech, "\"'")) {
                if (quoted) throw "can't read quoted string";
                else ech = " \0\n;(){}|";
            }
            else {
                quoted = true;
                pos.pos++;
            }
            ech += disabledChars;
            var escape = false;
            var res = "";
            while (!charIs(script.charAt(pos.pos), ech) || escape) {
                var c = script.charAt(pos.pos);
                if (pos.pos > pos.endpos) break;
                if (c == "") break;
                if (escape) {
                    if (c == "`") res += "`";
                    else if (c == '"') res += '"';
                    else if (c == "'") res += "'";
                    else if (c == "n") res += "\n";
                    else if (c == "r") res += "\r";
                    else if (c == "0") res += "\0";
                    else res += c;
                    escape = false;
                }
                else if (c == "`")
                    escape = true;
                else if (charIs(c, ech))
                    break;
                else res += c;
                pos.pos++;
            }
            if (!charIs(script.charAt(pos.pos), ech) && quoted) throw "incomplete_string";
            if (quoted) pos.pos++;
            return new StringCreate(res);
        }

        public OperatorsToCommand(ops: Array<{ op: string; val: ICommand }>): ICommand {
            if (ops.length == 0)
                return null;
            if (ops.length == 1) {
                if (ops[0].op == "")
                    return ops[0].val;
                else return new UnaryOperator(ops[0].op, ops[0].val);
            }
            if (ops[0].op != "") {
                ops[0] = { op: "", val: new UnaryOperator(ops[0].op, ops[0].val) };
            }

            this.operators.forEach(o => {
                var actualOp: Operator = null;
                for (var i = 0; i < ops.length; i++) {
                    if (i != 0) {
                        if (ops[i].op == "" && i != 0) throw "invalid_fromat";
                        if (actualOp == null) {
                            if ($.inArray(ops[i].op, o) >= 0)
                                actualOp = new Operator(ops[i].op, [ops[i - 1].val, ops[i].val]);
                        }
                        else if (ops[i].op == actualOp.operator) {
                            actualOp.commands.push(ops[i].val);
                        }
                        //kdyz je tu jiny operator z aktualni skupiny
                        else if ($.inArray(actualOp.operator, o) >= 0) {
                            //pocatecnimu prvku aktualni grupy priradit zgrupovanou verzi
                            ops[i - actualOp.commands.length].val = actualOp;
                            //posunout pozici za ní
                            i -= actualOp.commands.length - 1;
                            //a oriznout to zbyle
                            ops.splice(i, actualOp.commands.length - 1);
                            //aktualni bude zase {prave udelana grupa} {operator na i} {element na i}
                            actualOp = new Operator(ops[i].op, [ops[i - 1].val, ops[i].val]);
                        }
                        //je tu jiny operator (s nizsi prioritou)
                        else {
                            //pocatecnimu prvku aktualni grupy priradit zgrupovanou verzi
                            ops[i - actualOp.commands.length].val = actualOp;
                            //posunout pozici za ní
                            i -= actualOp.commands.length - 1;
                            //a oriznout to zbyle
                            ops.splice(i, actualOp.commands.length - 1);
                            //aktualni bude zase {prave udelana grupa} {operator na i} {element na i}
                            actualOp = null;
                        }
                    }
                }
                if (actualOp != null) {
                    ops[ops.length - actualOp.commands.length].val = actualOp;
                    ops.splice(ops.length - actualOp.commands.length + 1, actualOp.commands.length - 1);
                }
            });
            return ops[0].val;
        }

        public ReadDotNotation(script: string, on: ICommand, pos = new Pos(script.length - 1)) {
            var ns = pos.pos;
            while (charIsLetterNumberOr(script.charAt(pos.pos), "_")) { pos.pos++; }
            var name = script.substring(ns, pos.pos);
            trim(script, pos);
            if (script.charAt(pos.pos) == "(") {
                var params = this.ReadMethodParameters(script, pos);
                pos.pos++;
                return new MethodCall(on, name, params);
            }
            return new Property(on, name);
        }

        public ReadMethodParameters(script: string, pos: Pos): Array<ICommand> {
            if (script.charAt(pos.pos) == "(") { pos.pos++; }
            else throw "'(' expected at position " + pos.pos;
            var params = new Array<ICommand>();
            while (!charIs(script.charAt(pos.pos), ");\n}")) {
                if(script.charAt(pos.pos) == ",") pos.pos++;
                trimc(script, pos, " \n\t");
                params.push(this.ReadExpression(script, pos, "),"));
                trim(script, pos);
            }
            return params;
        }

        public ReadParameters(script: string, pos: Pos): any {
            var result = {};
            var parameterPosition = 0;
            while (pos.pos < pos.endpos) {
                trim(script, pos);
                var name = "_pos_" + parameterPosition;
                var value;
                //když tu končí nějaké závorky nebo příkaz, tak ukončit čtení
                if (charIs(script.charAt(pos.pos), ")}|\n;")) break;
                //jestli je tu jméno přečíst ho
                if (script.charAt(pos.pos) == "-") {
                    var ns = ++pos.pos;
                    while (charIsLetter(script.charAt(pos.pos))) pos.pos++;
                    name = script.substring(ns, pos.pos);
                    while (script.charAt(pos.pos) == " " && pos.pos <= pos.endpos) pos.pos++;
                }
                //pokud tu neni takove pismenko (konec prikazu, konec skriptu nebo zacatek jmena), precist hodnotu
                if (!charIs(script.charAt(pos.pos), ")}|\n;-")) {
                    value = this.ReadParameterValue(script, pos);
                }
                result[name] = value;
            }
            return result;
        }

        public ReadParameterValue(script: string, pos = new Pos(script.length - 1)): ICommand {
            trim(script, pos);
            //když začíná písmenkem, číslem nebo dolarem přešíst zvláštním způsobem
            if (charIsLetter(script.charAt(pos.pos)) || charIsNumber(script.charAt(pos.pos)) || charIs(script.charAt(pos.pos), '$/*-+_')) {
                var ns = pos.pos;
                while (!charIs(script.charAt(pos.pos), " (){}|\n;") && pos.pos <= pos.endpos) pos.pos++;
                var str = script.substring(ns, pos.pos);
                var num: number;
                if (!isNaN(num = parseFloat(str)))
                    return new NumberCreate(num);
                var regexp = /(([0-9]+)|(\$.*)|(\(.*\))(\+)|(\-)|(\*)|(\/)|,)/;
                if (str.match(regexp)) {
                    return this.ReadExpression(str, new Pos(str.length - 1), " ");
                }
                else return new StringCreate(str);
            }
            //jinak přečíst normálně jako příkaz
            return this.ReadCommand(script, pos);
        }

        public ReadHashtable(script: string, pos = new Pos(script.length - 1)): ICommand {
            var fields = new Array< { name: StringCreate; value: ICommand }>();
            trim(script, pos);
            while (script.charAt(pos.pos) != '}') {
                // read key
                var name = this.ReadString(script, pos, false, ";=");
                trim(script, pos);
                if (script.charAt(pos.pos) != "=") throw "invalid hashtable format: '=' expected";
                pos.pos++;
                trim(script, pos);

                //read value
                var value = this.ReadExpression(script, pos);

                trim(script, pos);
                if (script.charAt(pos.pos) == ";" || script.charAt(pos.pos) == "\n")
                    pos.pos++;
                else if (script.charAt(pos.pos) != "}")
                    throw "invalid hashtable format: ';' or '}' expected";

                fields.push({ name: name, value: value });
                trim(script, pos);
            }
            return new NewHashtable(fields);
        }

        keywordParsers: { [keyword: string]: (script: string, pos: Pos) => ICommand; } = {
            "if": (script: string, pos: Pos) => {
                var condition = this.ReadParameterValue(script, pos);
                trim(script, pos);
                if (script.charAt(pos.pos) != "{")
                    throw this.incompleteScriptError;
                pos.pos++;
                var ifScript = this.ReadScript(script, pos);
                pos.pos++;
                var elseScript = new Script();
                trim(script, pos);
                if (isHere(script, pos, "else")) {
                    pos.pos += "else".length;
                    trim(script, pos);
                    if (script.charAt(pos.pos) != "{") throw "invalid_format: '{' expected";
                    pos.pos++;
                    elseScript = this.ReadScript(script, pos);
                }
                return new Condition(condition, ifScript, elseScript);
            },
            "function": (script: string, pos: Pos) => {
                trim(script, pos);
                var fname = this.ReadString(script, pos, false).value;
                var parameters = new Array<FuncParameterDefinition>();
                trim(script, pos);
                if (script.charAt(pos.pos) == "(") {
                    pos.pos++;
                    trim(script, pos);
                    while (script.charAt(pos.pos) != ")") {
                        if (script.charAt(pos.pos) == "$") {
                            pos.pos++;
                            var ns = pos.pos;
                            while (charIsLetterNumberOr(script.charAt(pos.pos), "_$^")) { pos.pos++; }
                            var name = script.substring(ns, pos.pos);
                            parameters.push(new FuncParameterDefinition(name));
                        }
                        else throw "invalid token, '$' expected";
                        trim(script, pos);
                        if (script.charAt(pos.pos) == ",") pos.pos++;
                        else if (script.charAt(pos.pos) != ")") throw "',' or ')' expected at position " + pos.pos;
                        trim(script, pos);
                    }
                    pos.pos++;
                }
                trim(script, pos);
                var s = this.ReadScriptBlock(script, pos).script;
                return new DefineFunction(FunctionDefinition.FromScript(s, fname, parameters));
            }
        };

        public ReadKeywordBlock(word: string, script: string, pos = new Pos(script.length - 1)): ICommand {
            return this.keywordParsers[word](script, pos);
        }
    }
}